"""-----------------------------------------------------------------------------
manage.py

This module consists of 3 main components: the Behavior class hierarchy, the
definition of State, and the Manager class. manage.py chooses what type of
Behavior object to create and then passes this object(along with state and input
as needed) to decider and present.
-----------------------------------------------------------------------------"""
import present
from present import FactSpec
from decider import choose
from decider import isEmptyFact
from understand import get_meaning
import knowledge
from knowledge import Fact

import random, threading, collections, re, sys
from time import sleep
from select import select

# global variables for communication across threads
inp = ""
inputf = threading.Event()
exitf = threading.Event()

#list of topic names
topicList = []
topicDict = {}

def output(*strings):
  for s in strings:
    print str(s),
  print

def debug(*strings):
  return
  for s in strings:
    print str(s),
  print

def checkin(input):
  return input

"""-----------------------------------------------------------------------------
Behavior - superclass for all behaviors.

This superclass used to contain fields for transitions, but now user-initiated
transitions are handled different.

present() - dummy function which all derived Behaviors redefine. Necessary(?)

__init__() is undefined because only empty Behaviors should ever be created(?).
-----------------------------------------------------------------------------"""
class Behavior(object):
    def present(self):
        pass
    
    def __repr__(self):
        return "Behavior()"
    
"""-----------------------------------------------------------------------------
Transition - the behavior in which the agent asks the user whether they would 
like to continue learning about the current subject or transition to another.

commonality - a string describing the relationship between fromFact and 
newFact.
-----------------------------------------------------------------------------"""

class Transition(Behavior):
    newFact = FactSpec()
    fromFact = FactSpec()
    commonality = ""
    
    def present(self):
        return present.transition(self)
    
    def __repr__(self):
        return "Transition()"

"""-----------------------------------------------------------------------------
Query - Behavior in which the agent asks the user whether they know a certain 
fact.

-----------------------------------------------------------------------------"""
class Query(Behavior):
    fact = FactSpec()
    
    def present(self):
        return present.query(self)
        
    def __repr__(self):
        return "Query()"
        
"""-----------------------------------------------------------------------------
DeclareSingle - Behavior in which the agent states a single fact.
-----------------------------------------------------------------------------"""
class DeclareSingle(Behavior):
    fact = FactSpec() 

    def present(self):
        return present.declareSingle(self)
        
    def __repr__(self):
        return "DeclareSingle()"

"""-----------------------------------------------------------------------------
DeclareTriple - Behavior in which the agent states exactly three facts.  
May change to state n facts in future.

facts is a list of exactly 3 FactSpec objects to be presented.
commonality is a string(type subject to change) indicating how the facts are
related.
-----------------------------------------------------------------------------"""
class DeclareTriple(Behavior):
    facts = [FactSpec(), FactSpec(), FactSpec()]
    
    def present(self):
        return present.declareTriple(self)
    
        
    def __repr__(self):
        return "DeclareTriple()"

"""-----------------------------------------------------------------------------
State - class representing state for use by manage.py and decider.py

variables primarily used by decider.py:
factsPresented - list of FactSpecs
pagesVisited - list of strings(?)
currentPage - string(?)
newPage - string set by decider on Transition behaviors(?)

variables used by manage.py in choosing behaviors:
lastTransition/Query/Declare - number of Behaviors chosen since last Transition
/Query/Declare
pageQueries = # of queries since last transition
knownQueries = # of queries known since last transition
p = list of probabilities associated with each behaviors.  Association is
    [Transition, Query, DeclareSingle, DeclareTriple]. Always sum to 100.
-----------------------------------------------------------------------------"""
class State(object):
    factsPresented = list()
    pagesVisited = list()
    currentPage = ""
    newPage = ""
    pageList = None
    factList = None
    
    lastTransition = 0
    lastQuery = 0
    lastDeclare = 0
    p = [0, 50, 40, 10]
    pageQueries = 0
    knownQueries = 0

    def __init__(self):
      self.factsPresented = []
      self.pagesVisited = []
      self.p = [0, 50, 40, 10]


"""-----------------------------------------------------------------------------
pickBehavior - returns Behavior object chosen randomly using list of prob.'s

Equations imply some basics about behavior:
-Topic must be discussed for at least 5 "turns."  Changing too quickly, even if
it happens at low probability is easiest way to make bot look stupid.
-Currently first Behavior on new subject is always a Query. Might try:
-- first term = min(100 - x^2.5, 0) which has effect of making the "rapidly 
  assess knowledge after subject change" idea more pronounced
-- something like 25cos(x/pi) + 25 so there is ebb-and-flow to assess/teach
-DeclareSingle is always 4x more likely than DeclareTriple
-Probability of Transition will be positive after 25 facts regardless of how 
 much user knows.  Probably won't be a problem.
 
Probabilities may seem low, but they are iterated upon, so I am guesstimating
that they might be okay.  Need to experiment.
-----------------------------------------------------------------------------"""
def pickBehavior(s):

    #temporary behavior logic for testing
    blist = [Transition(), DeclareSingle(), DeclareTriple(), Query()]
    if s.lastTransition > 0:
        return random.choice(blist)
    else:
        return random.choice(blist[1:])

    # pick transition probability
    """if s.lastTransition < 5:
        s.p[0] = 0
    else:
        s.p[0] = s.lastTransition * .5 + (s.knownQueries/s.pageQueries-.5) * 25
    # query probability
    s.p[1] = 100 * (s.pageQueries + 1)**-1.5 + s.lastQuery * 10
    # declare probability
    s.p[2] = 0.8 * (100 - s.p[0] - s.p[1])
    s.p[3] = 100 - s.p[0] - s.p[1] - s.p[2]
    # pick using weights.  Easier way?
    b = random.uniform(0, 100)
    #if b >= 0 and b < s.p[0]:
    if True:
        return Transition()
    if b >= s.p[0] and b < s.p[0] + s.p[1]:
        return Query()
    if b >= s.p[0] + s.p[1] and b < s.p[0] + s.p[1] + s.p[2]:
        return DeclareSingle()
    return DeclareTriple()"""
    


"""-----------------------------------------------------------------------------
updateState - takes state, behavior, and a string returned by understand and
changes all elements of state which manage.py should ever change except
currentPage, which is changed in checkTransition().
-----------------------------------------------------------------------------"""
def updateState(state, b, s):
	if s == "quit":
		exitf.set()
		return
	if isinstance(b, Transition):
		state.lastTransition = 0
		state.lastQuery += 1
		state.lastDeclare += 1
	elif isinstance(b, Query):
		state.lastTransition += 1
		state.lastQuery = 0
		state.lastDeclare += 1
		state.pageQueries += 1
		if s == "yes":
			state.knownQueries += 1
	else:
		state.lastTransition += 1
		state.lastQuery += 1
		state.lastDeclare = 0

# returns true if user has typed anything, doesn't work in windows
# haven't tested or used yet
def poll():
    return bool(len(select(sys.stdin, None, None)[0]))

"""-----------------------------------------------------------------------------
nag - waits on the user different amount of time depending on behavior.

Very easy improvement is to add a bunch of different strings

Could also add things like 10% chance to say "Isn't that interesting?" or 
something on Declare behaviors after a few seconds of no input.
-----------------------------------------------------------------------------"""
def nag(b, inputThread):
    global inputf
    
    if isinstance(b, Transition):
        inputf.wait(30)
        if not inputf.isSet():
            output("I guess not..")
    if isinstance(b, Query):
        inputf.wait(20)
        # currently input is mandatory for queries.  I don't have an opinion as
        # to whether it should be or not.
        while not inputf.isSet():
            output("Are you there? ", b.present())
            inputf.wait(25)
    if isinstance(b, DeclareSingle):
        inputf.wait(20)
    if isinstance(b, DeclareTriple):
        inputf.wait(35)
    inputf.clear()
    return


"""-----------------------------------------------------------------------------
Spell-checking functions - see http://norvig.com/spell-correct.html to see how
they work. correct() returns its input or a valid word within edit distance of 2
Very easy to increase to edit distance of 3, but might slow down program a lot.
Only checks edit distance if word is sufficiently long.  Otherwise pants = Dante
and poop = Pope :/
-----------------------------------------------------------------------------"""
alphabet = 'abcdefghijklmnopqrstuvwxyz'
digits = "0123456789s"
NWORDS = {}

def words(text): return re.findall('[a-z]+[a-z]+[a-z]+', text.lower()) 

def train(features):
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
    return model

def edits1(word):
    splits     = [(word[:i], word[i:]) for i in range(len(word) + 1)]
    deletes    = [a + b[1:] for a, b in splits if b]
    transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]
    replaces   = [a + c + b[1:] for a, b in splits for c in alphabet if b]
    inserts    = [a + c + b     for a, b in splits for c in alphabet]
    return set(deletes + transposes + replaces + inserts)

def dedits1(word):
    splits     = [(word[:i], word[i:]) for i in range(len(word) + 1)]
    deletes    = [a + b[1:] for a, b in splits if b]
    transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]
    replaces   = [a + c + b[1:] for a, b in splits for c in digits if b]
    inserts    = [a + c + b     for a, b in splits for c in digits]
    return set(deletes + transposes + replaces + inserts)

def known_edits2(word):
    return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

def known(words): return set(w for w in words if w in NWORDS)


def correct(word):
    if word.isalpha() and len(word) > 4:
        candidates = known([word]) or known(edits1(word)) or [word]
        if len(word) > 5:
            candidates = candidates or known_edits2(word)
    else:
        candidates = known([word]) or known(dedits1(word)) or [word]
    max = 1
    maxw = word
    for w in candidates:
        if NWORDS[w] > max:
            maxw = w
            max = NWORDS[w]
    return maxw


"""-----------------------------------------------------------------------------
pickTopic - returns topic if there is one that is similar enough. It might be
better to search through facts or both topic names and facts.  This method
only searches through topic names.

I am unsure how this module will end up accessing Page objects.  Hopefully
there is a list of them somewhere that I can access.

Could be simplified greatly, probably.
-----------------------------------------------------------------------------"""
def pickTopic(meaning):
    global topicList
    bestMatch = topicList[0]
    bestSim = 0
    for vector in meaning[1:]:
        cvector = [correct(w) for w in vector]
        for topic in topicList:
            sim = calcSim(cvector, topic)
            if sim > bestSim:
                bestSim = sim
                bestMatch = topic
		bestVector = vector
    if bestSim >= 1:
        return bestMatch, bestVector
    return None, None


"""-----------------------------------------------------------------------------
calcSim - helper function that returns the similarity between two vectors of
strings.  The similarity is simply the number of words in the vector within
an edit distance of 2 of any word in the topic name.
-----------------------------------------------------------------------------"""
def calcSim(vector, topic):
    topic = topic.lower()
    sim = 0
    for i in range(0, len(vector)):
        vector[i] = r"\b" + vector[i].lower() + r"\b"
        if re.search(vector[i], topic) != None:
            sim += 1
    return sim


def goodPage(page):
	if page.empty:
		return False
	else:
		for fact in page.facts:
			if not isEmptyFact(fact):
				return True
		return False
    
"""-----------------------------------------------------------------------------
greet - greets the user with two randomly selected subjects.  Currently these
can't be years because we don't deal with the decade special case.
-----------------------------------------------------------------------------"""
def greet():
    print goodPage(topicDict["The Last Supper"])
    topic1 = random.choice(topicList)
    while not goodPage(topicDict[topic1]):
	    topic1 = random.choice(topicList)
    topic2 = random.choice(topicList)
    while not goodPage(topicDict[topic2]) and topic2 != topic1:
	    topic2 = random.choice(topicList)
    while topic1 == topic2: 
      topic2 = random.choice(topicList)
    facts = FactSpec(Fact(topic1), None, topic2)
    output(present.intro(facts))
    
    inputf.wait(40)
    while not inputf.isSet():
        output("Please tell me what you'd like to learn about.")
        inputf.wait(45)
    inputf.clear()


"""-----------------------------------------------------------------------------
checkTransition - asks the user if he wants to transition to a certain subject
and determines the response.  
-----------------------------------------------------------------------------"""
def checkTransition(meaning, state):
    if meaning[0] == "quit":
	    exitf.set()
    substrf = False
    if len(meaning) < 2:
        return
    
    newTopic, nounPhrase = pickTopic(meaning)
    if newTopic == None or not goodPage(topicDict[newTopic]):
        return
	
    for w in nounPhrase:
	    if w.lower() in newTopic.lower() and len(w) > 3:
		    substrf = True
    if not substrf:
	    facts = FactSpec(Fact(newTopic))
	    #ask and demand response
	    output(present.confirm(facts))
	    inputf.wait(30)
	    while not inputf.isSet():
		    output("Please answer.", present.confirm(facts))
		    inputf.wait(25)
	    inputf.clear()              
   #  analyze user's response
            meaning = get_meaning(inp)
	    if meaning[0] == "yes":
	        output("Okay. I will tell you about " + newTopic + ".")
	        state.lastTransition = 0
	        state.pageQueries = 0
	        state.knownQueries = 0
	        state.currentPage = topicDict[newTopic]
	    if meaning[0] == "no":
	        output("Then let's continue talking about".
	        state.currentPage.url.replace("_", " ") + ".")
            if meaning[0] == "unknown":
	        output("I don't understand.  Let's just talk about",
                state.currentPage.url.replace("_", " ") + ".")
	    if meaning[0] == "quit":
		    exitf.setf()
            sleep(1)
    else:
	      state.currentPage = topicDict[newTopic]


class InputThread(threading.Thread):
    def run(self):
        global inp
        while True:
            try: inp = checkin(raw_input())
            except EOFError:
                exitf.set()
            inputf.set()
            
"""-----------------------------------------------------------------------------
Manager - Manages Behaviors, contains and updates state, gets input.

converse - contains primary program loop and initialization
-----------------------------------------------------------------------------"""

class Manager:
    def __init__(self):
        global NWORDS
        global topicList
        global inputThread
        
        self.facts, self.pages = knowledge.load_facts()

        self.dates = filter(lambda d: isinstance(d, knowledge.Date), self.facts)
        self.facts = filter(lambda d: isinstance(d, knowledge.Fact), self.facts)

        # Create topicList
        for p in self.pages:
          topicList.append(p.url.replace('_', ' '))
          topicDict[p.url.replace('_', ' ')] = p
        NWORDS = train(words('\n'.join(p.url for p in self.pages)))

        inputThread = InputThread()
        inputThread.setDaemon(True)
        inputThread.start()

    def converse(self):
        state = State()
        state.currentPage = self.pages[0]
        state.pageList = filter(lambda p: not p.empty, self.pages)
        state.factList = self.facts

        greet()
        checkTransition(get_meaning(inp), state)

        while not exitf.isSet():
            b = pickBehavior(state)
            debug(type(b))
            try:
               choose(state, b)
            except ValueError:
               pageProblem(b, state)
               continue
            output(b.present())
            nag(b, inputThread)

            meaning = get_meaning(inp)
            updateState(state, b, meaning[0])

            if isinstance(b, Transition) and meaning[0] == "yes":
                state.pagesVisited.append(state.newPage)
                state.currentPage = state.newPage
                state.newPage = None

            checkTransition(meaning, state)
	    
	print present.bye()

def pageProblem(behavior, state):
   if type(behavior) == Transition:
      rand = random.choice(state.pageList)
      tries = 0
      while True:
         if rand in state.pagesVisited or rand.empty:
            rand = random.choice(state.pageList)
            tries = tries + 1
            if tries > 100:
	       exitf.set()
               return
         else:
            toBreak = False
            for fact in rand.facts:
               if not isEmptyFact(fact):
                  toBreak = True
                  break
            if toBreak:
               break
            rand = random.choice(state.pageList)
            tries = tries + 1
            if tries > 100:
	       exitf.set()
               return
      state.currentPage = rand
      state.pagesVisited.append(rand)
      print "Let's change the subject to " + str(rand)
   else:
      current = state.currentPage
      rand = random.choice(state.pageList)
      tries = 0
      while True:
         if rand in state.pagesVisited or rand.empty:
            rand = random.choice(state.pageList)
            tries = tries + 1
            if tries > 100:
	       exitf.set()
               return
         else:
            toBreak = False
            for fact in rand.facts:
               if not isEmptyFact(fact):
                  toBreak = True
                  break
            if toBreak:
               break
            rand = random.choice(state.pageList)
            tries = tries + 1
            if tries > 100:
               exitf.set()
	       return
      state.currentPage = rand
      state.pagesVisited.append(rand)
      print "Actually, I don't know anything more about " + \
            str(current) + ".  Let's talk about " + str(rand)

